/**
 * @filename WeChatAPIService
 *
 * @module WeChatAPIService
 *
 * @author Justin <zhou78620051@126.com>
 * @version 1
 * @time 2015/10/16 9:52
 */

var util = require("util");
var crypto = require("crypto");
var jsSHA = require('jssha');
var request = require("request");

var Logger = require('pomelo-logger').getLogger('WeChat', 'production');

var WeChatConfig = require("../configs/WeChatConfig");
var WeChatLockname = "wechat-access-token-lock";

module.exports = {
  /**
   * get access token from cache, if timeout fetch from wechat
   *
   * @param {String} appid APPID
   * @param {Function} next (err, info) info is WeChatToken model
   */
  getCachedAccessToken: function (appid, next) {
    var self = this;
    WeChatToken.findData(appid, function (err, info) {
      if (err) {
        next({"status": 10002001, "msg": "load data from db failed"});
        return;
      }
      if (!info || info.length == 0) {
        info = WeChatConfig[appid];
        info.token_timestamp = 0;
        info.ticket_timestamp = 0;
      } else {
        info = info[0];
      }

      // @todo 并发时第一个用户去取数据, 之后的用户的等待处理暂未添加
      if (info.token_timestamp + 7000000 <= new Date().valueOf()) {
        RedLockService.lock(WeChatLockname, function (err, lock) {
          var l = "Request RedLock For [ %s ] at [ %s ] is %s err: ";
          if (err) {
            Logger.error(l, WeChatLockname, new Date(), "Failed", err);
            process.nextTick(function () {
              self.getCachedAccessToken(appid, next);
            });
          } else {
            Logger.info(l, WeChatLockname, new Date(), "Success", err);

            WeChatAPIService.requestAccessToken(info, function (err, msg) {
              if (err) {
                next({"status": 10003001, "msg": "load access token from wechat api error"});
                lock.unlock();
                return;
              }
              info.access_token = msg.access_token;
              info.token_timestamp = new Date().valueOf();

              WeChatAPIService.requestJsApiTicket(info, function (err, msg) {
                if (err) {
                  next({"status": 10003001, "msg": "load js api ticket from wechat api error"});
                  lock.unlock();
                  return;
                }
                info.jsapi_ticket = msg.ticket;
                info.ticket_timestamp = new Date().valueOf();
                WeChatAPIService.saveRequestedData(info);

                next(null, info);
                lock.unlock();
              });
            });
          }
        });
      } else if (info.ticket_timestamp + 7000000 <= new Date().valueOf()) {
        //WeChatAPIService.requestJsApiTicket(info, function (err, msg) {
        //  if (err) {
        //    next({"status": 10003001, "msg": "load js api ticket from wechat api error"});
        //    return;
        //  }
        //  info.jsapi_ticket = msg.ticket;
        //  info.ticket_timestamp = new Date().valueOf();
        //  WeChatAPIService.saveRequestedData(info);
        //  next(null, info);
        //});
        Logger.error("jsapi_ticket timeout but access token is ok, this is bad!!!");
        next(null, info);
      } else {
        next(null, info);
      }
    });
  },

  /**
   * 计算微信签名 signature
   *
   * see http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html 附录1
   *
   * @method calcSignature
   * @for WeChatAPIService
   * @param {String} jsapi_ticket jsapi_ticket
   * @param {String} url 网页当前url
   * @param {Function=} next
   * @returns {{jsapi_ticket: *, noncesstr: string, timestamp: string, url: *, signature: string}}
   */
  calcSignature: function (jsapi_ticket, url, next) {
    if (typeof next !== "function") {
      next = function () {
      };
    }

    var createNonceStr = function () {
      return Math.random().toString(36).substr(2, 15);
    };

    var createTimestamp = function () {
      return parseInt(new Date().getTime() / 1000) + '';
    };

    var raw = function (args) {
      var keys = Object.keys(args);
      keys = keys.sort();
      var newArgs = {};
      keys.forEach(function (key) {
        newArgs[key.toLowerCase()] = args[key];
      });

      var string = '';
      for (var k in newArgs) {
        string += '&' + k + '=' + newArgs[k];
      }
      string = string.substr(1);
      return string;
    };

    var ret = {
      jsapi_ticket: jsapi_ticket,
      nonceStr: createNonceStr(),
      timestamp: createTimestamp(),
      url: url
    };
    var string = raw(ret);
    var shaObj = new jsSHA(string, 'TEXT');
    ret.signature = shaObj.getHash('SHA-1', 'HEX');
    Logger.warn("=============", ret, string);
    next(null, ret);
    return ret;
  },

  /**
   * 根据两点坐标计算地球两点距离
   *
   * todo bug: 这个公式在大多数情况下比较正确，只有在处理球面上的相对点的时候，会出现问题，有一个修正的公式，因为没有需要，就没有找出来，可以在wiki上查到。
   *
   * see: http://www.cnblogs.com/cocowool/archive/2009/03/24/1420478.html
   *
   * @param {Number} lat1 纬度1
   * @param {Number} lng1 经度1
   * @param {Number} lat2 纬度2
   * @param {Number} lng2 经度2
   * @param {Function=} next
   * @returns {String} 两点间距离, 单位为米
   */
  calcDistance: function (lat1, lng1, lat2, lng2, next) {
    if (typeof next != "function") {
      next = function () {
      };
    }

    var EARTH_RADIUS = 6378137.0;    //单位M
    var PI = Math.PI;

    function getRad(d) {
      return d * PI / 180.0;
    }

    /**
     * caculate the great circle distance 把地球当成圆球计算, 有误差
     * @param {Object} lat1
     * @param {Object} lng1
     * @param {Object} lat2
     * @param {Object} lng2
     */
    function getGreatCircleDistance(lat1, lng1, lat2, lng2) {
      var radLat1 = getRad(lat1);
      var radLat2 = getRad(lat2);

      var a = radLat1 - radLat2;
      var b = getRad(lng1) - getRad(lng2);

      var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
      s = s * EARTH_RADIUS;
      s = Math.round(s * 10000) / 10000.0;

      return s;
    }

    /**
     * approx distance between two points on earth ellipsoid 把地球当成椭球计算, 较精确
     * @param {Object} lat1
     * @param {Object} lng1
     * @param {Object} lat2
     * @param {Object} lng2
     */
    function getFlatternDistance(lat1, lng1, lat2, lng2) {
      var f = getRad((lat1 + lat2) / 2);
      var g = getRad((lat1 - lat2) / 2);
      var l = getRad((lng1 - lng2) / 2);

      var sg = Math.sin(g);
      var sl = Math.sin(l);
      var sf = Math.sin(f);

      var s, c, w, r, d, h1, h2;
      var a = EARTH_RADIUS;
      var fl = 1 / 298.257;

      sg = sg * sg;
      sl = sl * sl;
      sf = sf * sf;

      s = sg * (1 - sl) + (1 - sf) * sl;
      c = (1 - sg) * (1 - sl) + sf * sl;

      w = Math.atan(Math.sqrt(s / c));
      r = Math.sqrt(s * c) / w;
      d = 2 * w * a;
      h1 = (3 * r - 1) / 2 / c;
      h2 = (3 * r + 1) / 2 / s;

      return d * (1 + fl * (h1 * sf * (1 - sg) - h2 * (1 - sf) * sg));
    }

    var distance = getFlatternDistance(lat1, lng1, lat2, lng2);
    next(null, distance);
    return distance;
  },

  /**
   * request js-sdk access token
   *
   * see http://mp.weixin.qq.com/wiki/11/0e4b294685f817b95cbed85ba5e82b8f.html
   *
   * @method requestAccessToken
   * @for WeChatAPIService
   * @param {Object} info {appid, secret}
   * @param {Function} next (err, {access_token, expires_in})
   * @async
   * @return {Null}
   */
  requestAccessToken: function (info, next) {
    var httpAuthAddr = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    var urlPath = util.format(httpAuthAddr, info.appid, info.secret);
    request(urlPath, function (err, response, body) {
      if (err || response.statusCode !== 200) {
        return next(err);
      }

      var msg;
      try {
        msg = JSON.parse(body);
      } catch (e) {
        Logger.error("parse json from wechat when load access_token [ %s ] [ %s ]", urlPath, body);
      }

      var error = null, l;
      if (msg && msg.errcode) {
        l = "load access_token return error code [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      } else if (msg && msg.access_token) {
        Logger.info("load access_token succeed! [ %s ] [ %s ]", urlPath, body);
      } else {
        l = "unkown error while load access token [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      }
      next(err, msg);
    });
  },

  /**
   * request js-sdk js api ticket
   *
   * see http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html 附录1
   *
   * @method requestJsApiTicket
   * @for WeChatAPIService
   * @param {Object} info {access_token}
   * @param {Function} next (err, {errcode, errmsg, ticket, expires_in})
   * @async
   * @return {Null}
   */
  requestJsApiTicket: function (info, next) {
    var httpAuthAddr = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi";
    var urlPath = util.format(httpAuthAddr, info.access_token);
    request(urlPath, function (err, response, body) {
      if (err || response.statusCode !== 200) {
        return next(err);
      }

      var msg;
      try {
        msg = JSON.parse(body);
      } catch (e) {
        Logger.error("parse json from wechat when load jsapi_ticket [ %s ] [ %s ]", urlPath, body);
      }

      var error = null, l;
      if (msg && msg.errcode) {
        l = "load jsapi_ticket return error code [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      } else if (msg && msg.ticket) {
        Logger.info("load jsapi_ticket succeed! [ %s ] [ %s ]", urlPath, body);
      } else {
        l = "unkown error while load jsapi ticket [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      }
      next(err, msg);
    });
  },

  /**
   * request wechat server list
   *
   * see http://mp.weixin.qq.com/wiki/0/2ad4b6bfd29f30f71d39616c2a0fcedc.html
   *
   * @method updateWeChatServerList
   * @for WeChatAPIService
   * @param {Object} info {access_token}
   * @param {Function=} next (err, {ip_list})
   * @async
   * @return {Null}
   */
  updateWeChatServerList: function (info, next) {
    if (typeof next !== "function") {
      next = function () {
      };
    }

    var httpAuthAddr = "https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=%s";
    var urlPath = util.format(httpAuthAddr, info.access_token);
    request(urlPath, function (err, response, body) {
      if (err || response.statusCode !== 200) {
        next(err);
        return;
      }

      var msg;
      try {
        msg = JSON.parse(body);
      } catch (e) {
        Logger.error("parse json from wechat when load wechat server list [ %s ] [ %s ]", urlPath, body);
        err = e;
      }

      next(err, msg);

      var l;
      if (msg && msg.ip_list) {
        l = "load wechat server list success [ %s ] [ %s ]";
        Logger.info(l, urlPath, body);
        WeChatToken.saveData(info.appid, {"ip_list": msg.ip_list}, function () {
          l = "save wechat server list [ %j ]";
          Logger.info(l, arguments);
        });
      } else {
        l = "unkown error while load wechat server list [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
      }
    });
  },

  /**
   * save wechat access data back into db
   *
   * @method saveRequestedData
   * @for WeChatAPIService
   * @param {Object} info struct see WeChatToken model
   * @return {Null}
   */
  saveRequestedData: function (info) {
    var self = this;
    WeChatToken.saveData(info.appid, info, function () {
      Logger.info("save requested data [ %j ]", arguments);
      self.updateWeChatServerList(info);
    });
  },


  /**
   * request access token for web auth
   *
   * see http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
   *
   * @method requestWebAccessToken
   * @for WeChatAPIService
   *
   * @param {String} appid APPID
   * @param {String} secret SECRET
   * @param {String} code 授权成功后附带的CODE
   * @param {String | Function} userid UserId 用户唯一标识符, 自己平台的用户ID标记
   * @param {Function=} next (err, msg)
   *
   *    {
   *      "access_token":"ACCESS_TOKEN",
   *      "expires_in":7200,
   *      "refresh_token":"REFRESH_TOKEN",
   *      "openid":"OPENID",
   *      "scope":"SCOPE",
   *      "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
   *    }
   *
   *    access_token  网页授权接口调用凭证,注意：此access_token与基础支持的access_token不同
   *    expires_in  access_token接口调用凭证超时时间，单位（秒）
   *    refresh_token  用户刷新access_token
   *    openid  用户唯一标识，请注意，在未关注公众号时，用户访问公众号的网页，也会产生一个用户和公众号唯一的OpenID
   *    scope  用户授权的作用域，使用逗号（,）分隔
   *    unionid  只有在用户将公众号绑定到微信开放平台帐号后，才会出现该字段。详见：获取用户个人信息（UnionID机制）
   *
   * @async
   * @return {Null}
   */
  requestWebAccessToken: function (appid, secret, code, userid, next) {
    var self = this;
    if (typeof userid == "function") {
      next = userid;
      userid = "";
    }

    if (!userid) {
      self._requestWebAccessToken(appid, secret, code, next);
      return;
    }

    // todo cache login access token, incase auth every time
    self._requestWebAccessToken(appid, secret, code, next);
  },
  _requestWebAccessToken: function (appid, secret, code, next) {
    var accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
    var urlPath = util.format(accessTokenURL, appid, secret, code);
    request(urlPath, function (err, response, body) {
      if (err || response.statusCode !== 200) {
        return next(err);
      }

      var msg;
      try {
        msg = JSON.parse(body);
      } catch (e) {
        Logger.error("parse json from wechat when load web access token [ %s ] [ %s ]", urlPath, body);
      }

      var error = null, l;
      if (msg && msg.errcode) {
        l = "load web access token return error code [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      } else if (msg && msg.access_token) {
        Logger.info("load web access token succeed! [ %s ] [ %s ]", urlPath, body);
      } else {
        l = "unkown error while load web access token [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      }
      next(err, msg);
    });
  },

  /**
   * refresh access token for web auth
   *
   * see http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
   *
   * @method refreshWebAccessToken
   * @for WeChatAPIService
   *
   * @param {String} appid APPID
   * @param {String} refresh_token 授权时返回的供重新刷新access_token的字符串
   * @param {String | Function} userid UserId 用户唯一标识符, 自己平台的用户ID标记
   * @param {Function=} next (err, msg)
   *
   * access_token  网页授权接口调用凭证,注意：此access_token与基础支持的access_token不同
   * expires_in  access_token接口调用凭证超时时间，单位（秒）
   * refresh_token  用户刷新access_token
   * openid  用户唯一标识
   * scope  用户授权的作用域，使用逗号（,）分隔
   *
   * {
   *     "access_token":"ACCESS_TOKEN",
   *     "expires_in":7200,
   *     "refresh_token":"REFRESH_TOKEN",
   *     "openid":"OPENID",
   *     "scope":"SCOPE"
   *  }
   *
   * @async
   * @return {Null}
   */
  refreshWebAccessToken: function (appid, refresh_token, userid, next) {
    var self = this;
    if (typeof userid == "function") {
      next = userid;
      userid = "";
    }

    if (!userid) {
      self._refreshWebAccessToken(appid, refresh_token, next);
      return;
    }

    self._refreshWebAccessToken(appid, refresh_token, next);
  },
  _refreshWebAccessToken: function (appid, refresh_token, next) {
    var refreshTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s";
    var urlPath = util.format(refreshTokenURL, appid, refresh_token);
    request(urlPath, function (err, response, body) {
      if (err || response.statusCode !== 200) {
        return next(err);
      }

      var msg;
      try {
        msg = JSON.parse(body);
      } catch (e) {
        Logger.error("parse json from wechat when refresh web access token [ %s ] [ %s ]", urlPath, body);
      }

      var error = null, l;
      if (msg && msg.errcode) {
        l = "refresh web access token return error code [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      } else if (msg && msg.access_token) {
        Logger.info("refresh web access token succeed! [ %s ] [ %s ]", urlPath, body);
      } else {
        l = "unkown error while refresh web access token [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      }
      next(err, msg);
    });
  },

  /**
   * request userinfo for web
   *
   * see http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
   *
   * @method requestUserInfo
   * @for WeChatAPIService
   *
   * @param {String} access_token 授权时返回的access_token
   * @param {String} openid 授权时返回的openid
   * @param {String | Function} userid UserId 用户唯一标识符, 自己平台的用户ID标记
   * @param {Function=} next
   *
   * openid  用户的唯一标识
   * nickname  用户昵称
   * sex  用户的性别，值为1时是男性，值为2时是女性，值为0时是未知
   * province  用户个人资料填写的省份
   * city  普通用户个人资料填写的城市
   * country  国家，如中国为CN
   * headimgurl  用户头像，最后一个数值代表正方形头像大小（有0、46、64、96、132数值可选，0代表640*640正方形头像），用户没有头像时该项为空。若用户更换头像，原有头像URL将失效。
   * privilege  用户特权信息，json 数组，如微信沃卡用户为（chinaunicom）
   * unionid  只有在用户将公众号绑定到微信开放平台帐号后，才会出现该字段。详见：获取用户个人信息（UnionID机制）
   *
   * {
   *   "openid":"OPENID",
   *   "nickname": "NICKNAME",
   *   "sex":"1",
   *   "province":"PROVINCE"
   *   "city":"CITY",
   *   "country":"COUNTRY",
   *   "headimgurl":"http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
   *   "privilege":[
   *       "PRIVILEGE1"
   *       "PRIVILEGE2"
   *    ],
   *    "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
   *  }
   *
   * @async
   * @return {Null}
   */
  requestUserInfo: function (access_token, openid, userid, next) {
    var self = this;
    if (typeof userid == "function") {
      next = userid;
      userid = "";
    }

    if (!userid) {
      self._requestUserInfo(access_token, openid, next);
      return;
    }
    self._requestUserInfo(access_token, openid, next);
  },
  _requestUserInfo: function (access_token, openid, next) {
    var loadUserInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN";
    var urlPath = util.format(loadUserInfoURL, access_token, openid);
    request(urlPath, function (err, response, body) {
      if (err || response.statusCode !== 200) {
        return next(err);
      }

      var msg;
      try {
        msg = JSON.parse(body);
      } catch (e) {
        Logger.error("parse json from wechat when load userinfo [ %s ] [ %s ]", urlPath, body);
      }

      var error = null, l;
      if (msg && msg.errcode) {
        l = "load userinfo return error code [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      } else if (msg && msg.openid) {
        Logger.info("load userinfo succeed! [ %s ] [ %s ]", urlPath, body);
      } else {
        l = "unkown error while load userinfo [ %s ] [ %s ]";
        Logger.error(l, urlPath, body);
        err = new Error(l, urlPath, body);
      }
      next(err, msg);
    });
  }
};
